/*
 * 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙，邂逅框架梦，本文采用木兰宽松许可证第2版]
 * 
 * https://zhiqim.org/project/zhiqim_kit/zhiqim_git.htm
 *
 * ZhiqimGit is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.zhiqim.git.http;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.zip.CRC32;
import java.util.zip.Deflater;

import org.zhiqim.git.Git;
import org.zhiqim.git.GitConstants;
import org.zhiqim.git.GitI18n;
import org.zhiqim.git.GitObjer;
import org.zhiqim.git.GitWalker;
import org.zhiqim.git.GitZiper;
import org.zhiqim.git.objer.ObjectLoader;
import org.zhiqim.git.objer.PackIndexWriter;
import org.zhiqim.git.objer.PackLock;
import org.zhiqim.git.objer.model.PersonIdent;
import org.zhiqim.git.objer.objectid.ObjectId;
import org.zhiqim.git.objer.objectid.PackedObjectId;
import org.zhiqim.git.objer.objectid.RevCommit;
import org.zhiqim.git.util.GitBytes;
import org.zhiqim.git.util.GitStreams;
import org.zhiqim.kernel.util.Asserts;
import org.zhiqim.kernel.util.Bytes;
import org.zhiqim.kernel.util.Ids;
import org.zhiqim.kernel.util.Validates;
import org.zhiqim.kernel.util.codes.SHA1;
import org.zhiqim.kernel.util.mutables.MBuf;
import org.zhiqim.kernel.util.mutables.MLong;
import org.zhiqim.kernel.util.seqs.Sequence;

/**
 * 接受包读写器
 *
 * @version v1.0.0 @author zouzhigang 2017-4-17 新建与整理
 */
public class ReceiveReader implements GitConstants
{
    private static final Sequence SEQ = new Sequence(5);
    
    private final Git git;
    private final GitWalker walker;
    private final GitObjer objer;
    private final PersonIdent ident;
    private final Progress progress;
    
    //三个复用的缓冲对象
    private MBuf buf;
    private MessageDigest digest;
    private CRC32 crc;
    
    //包文件索引&包文件
    private File tmpIdx;
    private File tmpPack;
    private RandomAccessFile file;
    
    //增量对象中的两种
    private List<ReceiveDeltaChain> baseByRef;
    private HashMap<Long, ReceiveDelta> baseByOfs;

    //两个缓冲流（已知展开后的大小，要先缓冲输入）
    private ReceiveInputStream input;
    private ReceiveFileStream stream;
    
    //对象总数&对象列表&增量对象数目&包文件校验码
    private List<PackedObjectId> objectList;
    private long objectCount;
    private int deltaCount;
    private byte[] packChecksum;

    /**
     * 构造函数
     * 
     * @param git           仓库
     * @param input         输入流
     * @param dstBase       目标文件
     * @throws IOException  异常
     */
    public ReceiveReader(Git git, PersonIdent ident, InputStream input, Progress progress) throws IOException
    {
        this.git = git;
        this.walker = new GitWalker(git);
        this.objer = git.getObjer();
        this.ident = ident;
        this.progress = progress;
        
        //复用对象
        this.buf = new MBuf();
        this.digest = SHA1.getInstance();
        this.crc = new CRC32();

        //文件
        String name = TMP_ + Ids.longId19(SEQ);
        this.tmpPack = new File(objer.getDirectory(), name + _PACK);
        this.tmpIdx = new File(objer.getDirectory(), name + _IDX);
        this.file = new RandomAccessFile(tmpPack, _RW_);
        this.file.setLength(0);
        
        //两个缓冲流
        this.input = new ReceiveInputStream(input, file);
        this.stream = new ReceiveFileStream(file);
    }
    
    /******************************************************************************/
    // 读取包内容
    /******************************************************************************/

    /**
     * 解析数据流，写入数据到临时包文件和索引文件中
     * 
     * @throws IOException  IO异常
     */
    public void parse() throws IOException
    {
        progress.start();
        
        try
        {
            //1.读输入流然后写入标准包到临时文件中
            readAndWriteStdPack();
            
            //2.写入增量包到临时文件中
            writeDeltaPack();

            //3.写入包文件完成
            writePackFinished();

            //4.写入包索引文件
            writeIdx();
        }
        catch (IllegalArgumentException | IOException e)
        {//已知的错误作清理操作
            cleanupTmpFiles();
            throw e;
        }
        catch(Error e)
        {//其他错误暂时不清理临时文件
            throw e;
        }
        finally
        {
            file.close();
            progress.endTask();
        }
    }
    
    /******************************************************************************/
    // 读取包内容
    /******************************************************************************/
    
    private void readAndWriteStdPack() throws IOException
    {
        //1.读包头
        objectCount = GitStreams.readPackHeader(input);

        objectList = new ArrayList<>();
        baseByRef = new ArrayList<>();
        baseByOfs = new HashMap<Long, ReceiveDelta>();

        //2.逐个读对象
        progress.beginTask(PROGRESS_DOWNLOAD, (int) objectCount);
        
        for (int i=0;i<objectCount;i++)
        {
            readObject();
            progress.update(1);
        }
        
        //3.读包校验码
        readPackFooter();
        
        //4.读包结束
        readEnd();
        
        progress.endTask();
    }

    /** 读对象（含增量） */
    private void readObject() throws IOException
    {
        long pos = input.position();
        input.resetCRC32();

        //读取对象头信息（包括对象类型和对象展开后字节长度）
        MLong len = new MLong();
        int type = GitStreams.readObjectHeader(input, len);
        
        switch (type)
        {
        case OBJ_COMMIT:
        {//提交对象
            readObjectCommit(type, pos, len.value);
            break;
        }
        case OBJ_TREE:
        case OBJ_BLOB:
        case OBJ_TAG:
        {//标准对象
            readObjectWhole(type, pos, len.value);
            break;
        }
        case OBJ_OFS_DELTA:
        {//偏移增量
            long ofs = GitStreams.readObjectOfsDelta(input);
            long base = pos - ofs;
            input.skipInflate(len.value);
            ReceiveDelta delta = new ReceiveDelta(pos, input.getCRC32());
            delta.next = baseByOfs.put(base, delta);
            deltaCount++;
            break;
        }
        case OBJ_REF_DELTA:
        {//引用增量
            ObjectId base = GitStreams.readObjectId(input);
            
            ReceiveDeltaChain chain = null;
            int ind = baseByRef.indexOf(base);
            if (ind != -1){
                chain = baseByRef.get(ind);
            }else{
                chain = new ReceiveDeltaChain(base);
                baseByRef.add(chain);
            }
            
            input.skipInflate(len.value);
            chain.addHead(new ReceiveDelta(pos, input.getCRC32()));
            deltaCount++;
            break;
        }
        default:
            throw Asserts.exception("非法的对象类型[%s]", type);
        }
    }
    
    /** 读整个对象 */
    private void readObjectCommit(int type, long offset, long len) throws IOException
    {
        byte[] data = input.inflate(len);
        ObjectId oid = ObjectId.parseContent(type, data);
        
        if (ident != null)
        {
            RevCommit commit = new RevCommit(oid);
            commit.parseCanonical(walker, data);
            Asserts.as(ident.getName().equals(commit.getAuthorIdent().getName())?null:"提交作者["+commit.getAuthorIdent().getName()+"]和操作员账号["+ident.getName()+"]不一致，请修改作者名称后提交");
            Asserts.as(ident.getName().equals(commit.getCommitterIdent().getName())?null:"提交者["+commit.getAuthorIdent().getName()+"]和操作员账号["+ident.getName()+"]不一致，请修改提交者名称后提交");
            
            if (Validates.isNotEmpty(ident.getEmail()))
            {
                Asserts.as(ident.getEmail().equals(commit.getAuthorIdent().getEmail())?null:"提交作者邮箱["+commit.getAuthorIdent().getEmail()+"]和操作员邮箱["+ident.getEmail()+"]不一致，请修改提交作者邮箱后提交");
                Asserts.as(ident.getEmail().equals(commit.getCommitterIdent().getEmail())?null:"提交者邮箱["+commit.getAuthorIdent().getEmail()+"]和操作员邮箱["+ident.getEmail()+"]不一致，请修改提交者邮箱后提交");
            }
        }

        chkObject(oid, type, data);
        objectList.add(new PackedObjectId(oid, offset, input.getCRC32()));
    }
    
    /** 读整个对象 */
    private void readObjectWhole(int type, long offset, long len) throws IOException
    {
        byte[] data = input.inflate(len);
        ObjectId oid = ObjectId.parseContent(type, data);

        chkObject(oid, type, data);
        objectList.add(new PackedObjectId(oid, offset, input.getCRC32()));
    }
    
    /** 读包校验码 */
    private void readPackFooter() throws IOException
    {
        input.sync();
        byte[] calcChecksum = input.getDigest();
        byte[] readChecksum = input.readBytesNoCRC(LEN_OBJECT_ID);
        
        Asserts.asserts(Arrays.equals(calcChecksum, readChecksum), "传入的PACK文件校验码不正确");
        
        if (deltaCount == 0)
        {//如果没有增量对象，则在文件最后增加校验码
            packChecksum = readChecksum;//索引文件也需要
            file.write(packChecksum);
        }
    }

    /** 读包结束，清理 */
    private void readEnd()
    {
        input = null;
    }

    /** 对存在的对象检查是否一致 */
    private void chkObject(ObjectId id, int type, byte[] data) throws IOException
    {
        ObjectLoader ldr = objer.getObjectLoader(id);
        if (ldr == null)
            return;
        
        if (ldr.getType() != type)
            throw Asserts.exception("对象冲突[%s][类型不一致]", id.getName());
        
        byte[] existingData = ldr.getData();
        if (!Arrays.equals(data, existingData))
            throw Asserts.exception("对象冲突[%s][数据不一致]", id.getName());
    }

    /******************************************************************************/
    // 解析和处理增量包
    /******************************************************************************/

    private void writeDeltaPack() throws IOException
    {
        if (deltaCount == 0)
            return;
        
        //检查标准对象
        progress.beginTask(PROGRESS_RESOLVE_DELTA, deltaCount);
        
        int size = objectList.size();
        for (int i=0;i<size;i++)
        {
            int before = objectList.size();
            resolveDeltas(objectList.get(i));
            progress.update(objectList.size() - before);
        }
        
        progress.endTask();
        
        fixThinPack();
    }
    
    private void resolveDeltas(PackedObjectId po) throws IOException
    {
        if (baseByRef.indexOf(po) != -1 || baseByOfs.containsKey(po.getOffset()))
        {
            resolveDelta(po.getOffset(), po.getCRC(), OBJ_BAD, null, po);
        }
    }

    /** 修正薄包对象 */
    private void fixThinPack() throws IOException
    {
        long offset = file.length();
        
        Deflater def = GitZiper.deflater(git.getConfig().getCompression());
        
        try
        {
            for (ReceiveDeltaChain baseId : baseByRef)
            {
                if (baseId.head == null)
                    continue;
                
                ObjectLoader ldr = git.getObjectLoader(baseId);
                Asserts.assertNotNull(ldr, GitI18n.objectNotFound, baseId.toString());
                
                int type = ldr.getType();
                byte[] data = ldr.getData();
    
                file.seek(offset);
                int crc = writeDeltaObject(def, type, data);
                PackedObjectId po = new PackedObjectId(baseId, offset, crc);
                objectList.add(po);
                offset = file.getFilePointer();
    
                //处理该基础对象下的所有增量包
                resolveChildDeltas(po, type, data);
            }
        }
        finally
        {
            GitZiper.close(def);
        }
        
        fixHeaderFooter();
    }
    
    /** 写入增量对象 */
    private int writeDeltaObject(Deflater def, int type, byte[] data) throws IOException
    {
        crc.reset();
        
        //1.计算对象头，写入头
        byte[] header = GitStreams.writeObjectHeader(type, data.length);
        crc.update(header);
        file.write(header);
        
        //2.压缩数据，写入内容
        def.reset();
        def.setInput(data);
        def.finish();
        while (!def.finished())
        {
            int count = def.deflate(buf.value);
            crc.update(buf.value, 0, count);
            file.write(buf.value, 0, count);
        }
        
        return (int)crc.getValue();
    }
    
    /** 处理增量包 */
    private void resolveDelta(long offset, int crc, int type, byte[] data, PackedObjectId po) throws IOException
    {
        //初始化校验码&基础位置
        stream.resetCRC32();
        stream.seek(offset);
        
        //读取对象头信息（包括对象类型和对象展开后字节长度）
        MLong len = new MLong();
        int typeCode = GitStreams.readObjectHeader(stream, len);
        
        switch (typeCode)
        {
        case OBJ_COMMIT:
        case OBJ_TREE:
        case OBJ_BLOB:
        case OBJ_TAG:
        {//标准对象
            type = typeCode;
            data = stream.inflate(len.value);
            break;
        }
        case OBJ_OFS_DELTA:
        {//偏移增量
            stream.skipOfs();
            data = GitBytes.merge(data, stream.inflate(len.value));
            break;
        }
        case OBJ_REF_DELTA:
        {//引用增量
            stream.skip(LEN_OBJECT_ID);
            data = GitBytes.merge(data, stream.inflate(len.value));
            break;
        }
        default:
            throw Asserts.exception(GitI18n.illegalObjectType, typeCode);
        }

        int crc32 = stream.getCRC32();
        if (crc != crc32)
            throw Asserts.exception("处理增量包时对象校验码不一致[offset=%s]", offset);
        
        if (po == null)
        {
            ObjectId oid = ObjectId.parseContent(type, data);
            chkObject(oid, type, data);
            po = new PackedObjectId(oid, offset, crc32);
            objectList.add(po);
        }
        
        resolveChildDeltas(po, type, data);
    }

    private ReceiveDelta removeBaseById(final ObjectId id)
    {
        int ind = baseByRef.indexOf(id);
        return ind == -1?null:baseByRef.get(ind).removeHead();
    }

    private void resolveChildDeltas(PackedObjectId po, int type, byte[] data) throws IOException
    {
        ReceiveDelta a = ReceiveDelta.reverse(removeBaseById(po));
        ReceiveDelta b = ReceiveDelta.reverse(baseByOfs.remove(po.getOffset()));
        while (a != null && b != null)
        {
            if (a.offset < b.offset)
            {
                resolveDelta(a.offset, a.crc, type, data, null);
                a = a.next;
            }
            else
            {
                resolveDelta(b.offset, b.crc, type, data, null);
                b = b.next;
            }
        }
        
        resolveChildDeltaChain(a, type, data);
        resolveChildDeltaChain(b, type, data);
    }

    private void resolveChildDeltaChain(ReceiveDelta delta, int type, byte[] data) throws IOException
    {
        while (delta != null)
        {
            resolveDelta(delta.offset, delta.crc, type, data, null);
            delta = delta.next;
        }
    }
    
    /** 修正文件头部和尾部信息 */
    private void fixHeaderFooter() throws IOException
    {
        //1.修改文件头部对象数目
        file.seek(8);
        file.write(Bytes.BU.toBytes(objectList.size()));
        
        //2.重新生成文件校验码
        file.seek(0);
        digest.reset();
        
        while (true)
        {
            int count = file.read(buf.value);
            if (count < 0)
                break;

            digest.update(buf.value, 0, count);
        }
        
        //3.赋值包校验码并增加到包尾部
        packChecksum = digest.digest();
        file.write(packChecksum);
    }
    
    /******************************************************************************/
    // 读完成&写入包索引文件
    /******************************************************************************/

    private void writePackFinished() throws IOException
    {
        if (!objectList.isEmpty())
        {//保存并置为只读
            file.getChannel().force(true);
            tmpPack.setReadOnly();
        }
        
        //清理对象
        digest = null;
        baseByRef = null;
        baseByOfs = null;
    }
    
    private void writeIdx() throws IOException
    {
        if (objectList.isEmpty())
            return;
            
        //排序后写入到包索引文件
        Collections.sort(objectList);
        try (FileOutputStream os = new FileOutputStream(tmpIdx))
        {
            PackIndexWriter iw = new PackIndexWriter(os);
            iw.write(objectList, packChecksum);
            os.getChannel().force(true);
        }
        
        //设置为只读
        tmpIdx.setReadOnly();
    }
    
    /******************************************************************************/
    // 重命名&清理临时文件
    /******************************************************************************/

    /**
     * 重命名到最终格式的pack-${oid}.pack文件和pack-${oid}.idx文件
     * 
     * @param lockMessage   重命名时的锁信息（pack-${oid}.keep文件）
     * @return              包文件锁
     * @throws IOException  异常
     */
    public PackLock renameAndOpenPack(String lockMessage) throws IOException
    {
        if (objectList.isEmpty())
        {//没有实体对象
            cleanupTmpFiles();
            return null;
        }

        File packDir = objer.getPackDirectory();
        if (!packDir.exists())
        {//对象包文件不存在（要求Git创建时就创建好了）
            cleanupTmpFiles();
            throw Asserts.exception("对象包目录不存在，不支持打包对象[%s]", packDir.getAbsolutePath());
        }
        
        //计算包名称
        MessageDigest md = SHA1.getInstance();
        byte[] oidBuf = new byte[LEN_OBJECT_ID];
        for (PackedObjectId oid : objectList)
        {
            oid.copyRawTo(oidBuf, 0);
            md.update(oidBuf);
        }
        
        String name = ObjectId.parse(md.digest()).getName();
        File finalPack = new File(packDir, "pack-" + name + ".pack");
        File finalIdx = new File(packDir, "pack-" + name + ".idx");
        PackLock keep = new PackLock(finalPack);

        if (finalPack.exists())
        {//包已存在
            cleanupTmpFiles();
            return null;
        }

        if (lockMessage != null)
        {
            try
            {
                if (!keep.lock(lockMessage))
                    throw new IOException("重命名包文件锁包异常 " + finalPack);
            }
            catch (IOException e)
            {
                cleanupTmpFiles();
                throw e;
            }
        }

        if (!tmpPack.renameTo(finalPack))
        {//重命名包文件失败
            cleanupTmpFiles();
            keep.unlock();
            throw Asserts.exception("重命名包文件失败[%s]", finalPack);
        }

        if (!tmpIdx.renameTo(finalIdx))
        {//重命名索引文件失败
            cleanupTmpFiles();
            keep.unlock();
            if (!finalPack.delete())
                finalPack.deleteOnExit();
            
            throw Asserts.exception("重命名索引文件失败[%s]", finalIdx);
        }

        //写入更新缓存
        objer.updateIndexMap(finalIdx.getName());
        return lockMessage != null ? keep : null;
    }

    /** 清理临时文件 */
    private void cleanupTmpFiles()
    {
        if (!tmpIdx.delete() && tmpIdx.exists())
            tmpIdx.deleteOnExit();
        
        if (!tmpPack.delete() && tmpPack.exists())
            tmpPack.deleteOnExit();
    }
}